/* eslint no-prototype-builtins: 0 */
/**
 * jQuery-csv (jQuery Plugin)
 *
 * This document is licensed as free software under the terms of the
 * MIT License: http://www.opensource.org/licenses/mit-license.php
 *
 * Acknowledgements:
 * The original design and influence to implement this library as a jquery
 * plugin is influenced by jquery-json (http://code.google.com/p/jquery-json/).
 * If you're looking to use native JSON.Stringify but want additional backwards
 * compatibility for browsers that don't support it, I highly recommend you
 * check it out.
 *
 * A special thanks goes out to rwk@acm.org for providing a lot of valuable
 * feedback to the project including the core for the new FSM
 * (Finite State Machine) parsers. If you're looking for a stable TSV parser
 * be sure to take a look at jquery-tsv (http://code.google.com/p/jquery-tsv/).

 * For legal purposes I'll include the "NO WARRANTY EXPRESSED OR IMPLIED.
 * USE AT YOUR OWN RISK.". Which, in 'layman's terms' means, by using this
 * library you are accepting responsibility if it breaks your code.
 *
 * Legal jargon aside, I will do my best to provide a useful and stable core
 * that can effectively be built on.
 *
 * Copyrighted 2012 by Evan Plaice.
 */

RegExp.escape = function (s) {
  return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
};

(function () {
  'use strict'

  let $

  // to keep backwards compatibility
  if (typeof jQuery !== 'undefined' && jQuery) {
    $ = jQuery
  } else {
    $ = {}
  }

  /**
   * jQuery.csv.defaults
   * Encapsulates the method paramater defaults for the CSV plugin module.
   */

  $.csv = {
    defaults: {
      separator: ',',
      delimiter: '"',
      headers: true
    },

    hooks: {
      castToScalar: function (value, state) {
        const hasDot = /\./
        if (isNaN(value)) {
          return value
        } else {
          if (hasDot.test(value)) {
            return parseFloat(value)
          } else {
            const integer = parseInt(value)
            if (isNaN(integer)) {
              return null
            } else {
              return integer
            }
          }
        }
      }
    },

    parsers: {
      parse: function (csv, options) {
        // cache settings
        const separator = options.separator
        const delimiter = options.delimiter

        // set initial state if it's missing
        if (!options.state.rowNum) {
          options.state.rowNum = 1
        }
        if (!options.state.colNum) {
          options.state.colNum = 1
        }

        // clear initial state
        const data = []
        let entry = []
        let state = 0
        let value = ''
        let exit = false

        function endOfEntry () {
          // reset the state
          state = 0
          value = ''

          // if 'start' hasn't been met, don't output
          if (options.start && options.state.rowNum < options.start) {
            // update global state
            entry = []
            options.state.rowNum++
            options.state.colNum = 1
            return
          }

          if (options.onParseEntry === undefined) {
            // onParseEntry hook not set
            data.push(entry)
          } else {
            const hookVal = options.onParseEntry(entry, options.state) // onParseEntry Hook
            // false skips the row, configurable through a hook
            if (hookVal !== false) {
              data.push(hookVal)
            }
          }
          // console.log('entry:' + entry);

          // cleanup
          entry = []

          // if 'end' is met, stop parsing
          if (options.end && options.state.rowNum >= options.end) {
            exit = true
          }

          // update global state
          options.state.rowNum++
          options.state.colNum = 1
        }

        function endOfValue () {
          if (options.onParseValue === undefined) {
            // onParseValue hook not set
            entry.push(value)
          } else if (options.headers && options.state.rowNum === 1) {
            // don't onParseValue object headers
            entry.push(value)
          } else {
            const hook = options.onParseValue(value, options.state) // onParseValue Hook
            // false skips the row, configurable through a hook
            if (hook !== false) {
              entry.push(hook)
            }
          }
          // console.log('value:' + value);
          // reset the state
          value = ''
          state = 0
          // update global state
          options.state.colNum++
        }

        // escape regex-specific control chars
        const escSeparator = RegExp.escape(separator)
        const escDelimiter = RegExp.escape(delimiter)

        // compile the regEx str using the custom delimiter/separator
        let match = /(D|S|\r\n|\n|\r|[^DS\r\n]+)/
        let matchSrc = match.source
        matchSrc = matchSrc.replace(/S/g, escSeparator)
        matchSrc = matchSrc.replace(/D/g, escDelimiter)
        match = new RegExp(matchSrc, 'gm')

        // put on your fancy pants...
        // process control chars individually, use look-ahead on non-control chars
        csv.replace(match, function (m0) {
          if (exit) {
            return
          }
          switch (state) {
            // the start of a value
            case 0:
              // null last value
              if (m0 === separator) {
                value += ''
                endOfValue()
                break
              }
              // opening delimiter
              if (m0 === delimiter) {
                state = 1
                break
              }
              // null last value
              if (/^(\r\n|\n|\r)$/.test(m0)) {
                endOfValue()
                endOfEntry()
                break
              }
              // un-delimited value
              value += m0
              state = 3
              break

            // delimited input
            case 1:
              // second delimiter? check further
              if (m0 === delimiter) {
                state = 2
                break
              }
              // delimited data
              value += m0
              state = 1
              break

            // delimiter found in delimited input
            case 2:
              // escaped delimiter?
              if (m0 === delimiter) {
                value += m0
                state = 1
                break
              }
              // null value
              if (m0 === separator) {
                endOfValue()
                break
              }
              // end of entry
              if (/^(\r\n|\n|\r)$/.test(m0)) {
                endOfValue()
                endOfEntry()
                break
              }
              // broken paser?
              throw Error('CSVDataError: Illegal State [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')

            // un-delimited input
            case 3:
              // null last value
              if (m0 === separator) {
                endOfValue()
                break
              }
              // end of entry
              if (/^(\r\n|\n|\r)$/.test(m0)) {
                endOfValue()
                endOfEntry()
                break
              }
              if (m0 === delimiter) {
              // non-compliant data
                throw Error('CSVDataError: Illegal Quote [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
              }
              // broken parser?
              throw Error('CSVDataError: Illegal Data [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
            default:
              // shenanigans
              throw Error('CSVDataError: Unknown State [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
          }
          // console.log('val:' + m0 + ' state:' + state);
        })

        // submit the last entry
        // ignore null last line
        if (entry.length !== 0) {
          endOfValue()
          endOfEntry()
        }

        return data
      },

      // a csv-specific line splitter
      splitLines: function (csv, options) {
        if (!csv) {
          return undefined
        }

        options = options || {}

        // cache settings
        const separator = options.separator || $.csv.defaults.separator
        const delimiter = options.delimiter || $.csv.defaults.delimiter

        // set initial state if it's missing
        options.state = options.state || {}
        if (!options.state.rowNum) {
          options.state.rowNum = 1
        }

        // clear initial state
        const entries = []
        let state = 0
        let entry = ''
        let exit = false

        function endOfLine () {
          // reset the state
          state = 0

          // if 'start' hasn't been met, don't output
          if (options.start && options.state.rowNum < options.start) {
            // update global state
            entry = ''
            options.state.rowNum++
            return
          }

          if (options.onParseEntry === undefined) {
            // onParseEntry hook not set
            entries.push(entry)
          } else {
            const hookVal = options.onParseEntry(entry, options.state) // onParseEntry Hook
            // false skips the row, configurable through a hook
            if (hookVal !== false) {
              entries.push(hookVal)
            }
          }

          // cleanup
          entry = ''

          // if 'end' is met, stop parsing
          if (options.end && options.state.rowNum >= options.end) {
            exit = true
          }

          // update global state
          options.state.rowNum++
        }

        // escape regex-specific control chars
        const escSeparator = RegExp.escape(separator)
        const escDelimiter = RegExp.escape(delimiter)

        // compile the regEx str using the custom delimiter/separator
        let match = /(D|S|\n|\r|[^DS\r\n]+)/
        let matchSrc = match.source
        matchSrc = matchSrc.replace(/S/g, escSeparator)
        matchSrc = matchSrc.replace(/D/g, escDelimiter)
        match = new RegExp(matchSrc, 'gm')

        // put on your fancy pants...
        // process control chars individually, use look-ahead on non-control chars
        csv.replace(match, function (m0) {
          if (exit) {
            return
          }
          switch (state) {
            // the start of a value/entry
            case 0:
              // null value
              if (m0 === separator) {
                entry += m0
                state = 0
                break
              }
              // opening delimiter
              if (m0 === delimiter) {
                entry += m0
                state = 1
                break
              }
              // end of line
              if (m0 === '\n') {
                endOfLine()
                break
              }
              // phantom carriage return
              if (/^\r$/.test(m0)) {
                break
              }
              // un-delimit value
              entry += m0
              state = 3
              break

            // delimited input
            case 1:
              // second delimiter? check further
              if (m0 === delimiter) {
                entry += m0
                state = 2
                break
              }
              // delimited data
              entry += m0
              state = 1
              break

            // delimiter found in delimited input
            case 2: {
              // escaped delimiter?
              const prevChar = entry.substr(entry.length - 1)
              if (m0 === delimiter && prevChar === delimiter) {
                entry += m0
                state = 1
                break
              }
              // end of value
              if (m0 === separator) {
                entry += m0
                state = 0
                break
              }
              // end of line
              if (m0 === '\n') {
                endOfLine()
                break
              }
              // phantom carriage return
              if (m0 === '\r') {
                break
              }
              // broken paser?
              throw Error('CSVDataError: Illegal state [Row:' + options.state.rowNum + ']')
            }
            // un-delimited input
            case 3:
              // null value
              if (m0 === separator) {
                entry += m0
                state = 0
                break
              }
              // end of line
              if (m0 === '\n') {
                endOfLine()
                break
              }
              // phantom carriage return
              if (m0 === '\r') {
                break
              }
              // non-compliant data
              if (m0 === delimiter) {
                throw Error('CSVDataError: Illegal quote [Row:' + options.state.rowNum + ']')
              }
              // broken parser?
              throw Error('CSVDataError: Illegal state [Row:' + options.state.rowNum + ']')
            default:
              // shenanigans
              throw Error('CSVDataError: Unknown state [Row:' + options.state.rowNum + ']')
          }
          // console.log('val:' + m0 + ' state:' + state);
        })

        // submit the last entry
        // ignore null last line
        if (entry !== '') {
          endOfLine()
        }

        return entries
      },

      // a csv entry parser
      parseEntry: function (csv, options) {
        // cache settings
        const separator = options.separator
        const delimiter = options.delimiter

        // set initial state if it's missing
        if (!options.state.rowNum) {
          options.state.rowNum = 1
        }
        if (!options.state.colNum) {
          options.state.colNum = 1
        }

        // clear initial state
        const entry = []
        let state = 0
        let value = ''

        function endOfValue () {
          if (options.onParseValue === undefined) {
            // onParseValue hook not set
            entry.push(value)
          } else {
            const hook = options.onParseValue(value, options.state) // onParseValue Hook
            // false skips the value, configurable through a hook
            if (hook !== false) {
              entry.push(hook)
            }
          }
          // reset the state
          value = ''
          state = 0
          // update global state
          options.state.colNum++
        }

        // checked for a cached regEx first
        if (!options.match) {
          // escape regex-specific control chars
          const escSeparator = RegExp.escape(separator)
          const escDelimiter = RegExp.escape(delimiter)

          // compile the regEx str using the custom delimiter/separator
          const match = /(D|S|\n|\r|[^DS\r\n]+)/
          let matchSrc = match.source
          matchSrc = matchSrc.replace(/S/g, escSeparator)
          matchSrc = matchSrc.replace(/D/g, escDelimiter)
          options.match = new RegExp(matchSrc, 'gm')
        }

        // put on your fancy pants...
        // process control chars individually, use look-ahead on non-control chars
        csv.replace(options.match, function (m0) {
          switch (state) {
            // the start of a value
            case 0:
              // null last value
              if (m0 === separator) {
                value += ''
                endOfValue()
                break
              }
              // opening delimiter
              if (m0 === delimiter) {
                state = 1
                break
              }
              // skip un-delimited new-lines
              if (m0 === '\n' || m0 === '\r') {
                break
              }
              // un-delimited value
              value += m0
              state = 3
              break

            // delimited input
            case 1:
              // second delimiter? check further
              if (m0 === delimiter) {
                state = 2
                break
              }
              // delimited data
              value += m0
              state = 1
              break

            // delimiter found in delimited input
            case 2:
              // escaped delimiter?
              if (m0 === delimiter) {
                value += m0
                state = 1
                break
              }
              // null value
              if (m0 === separator) {
                endOfValue()
                break
              }
              // skip un-delimited new-lines
              if (m0 === '\n' || m0 === '\r') {
                break
              }
              // broken paser?
              throw Error('CSVDataError: Illegal State [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')

            // un-delimited input
            case 3:
              // null last value
              if (m0 === separator) {
                endOfValue()
                break
              }
              // skip un-delimited new-lines
              if (m0 === '\n' || m0 === '\r') {
                break
              }
              // non-compliant data
              if (m0 === delimiter) {
                throw Error('CSVDataError: Illegal Quote [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
              }
              // broken parser?
              throw Error('CSVDataError: Illegal Data [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
            default:
              // shenanigans
              throw Error('CSVDataError: Unknown State [Row:' + options.state.rowNum + '][Col:' + options.state.colNum + ']')
          }
          // console.log('val:' + m0 + ' state:' + state);
        })

        // submit the last value
        endOfValue()

        return entry
      }
    },

    helpers: {

      /**
       * $.csv.helpers.collectPropertyNames(objectsArray)
       * Collects all unique property names from all passed objects.
       *
       * @param {Array} objects Objects to collect properties from.
       *
       * Returns an array of property names (array will be empty,
       * if objects have no own properties).
       */
      collectPropertyNames: function (objects) {
        let o = []
        let propName = []
        const props = []
        for (o in objects) {
          for (propName in objects[o]) {
            if ((objects[o].hasOwnProperty(propName)) &&
                (props.indexOf(propName) < 0) &&
                (typeof objects[o][propName] !== 'function')) {
              props.push(propName)
            }
          }
        }
        return props
      }
    },

    /**
     * $.csv.toArray(csv)
     * Converts a CSV entry string to a javascript array.
     *
     * @param {Array} csv The string containing the CSV data.
     * @param {Object} [options] An object containing user-defined options.
     * @param {Character} [separator] An override for the separator character. Defaults to a comma(,).
     * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote(").
     *
     * This method deals with simple CSV strings only. It's useful if you only
     * need to parse a single entry. If you need to parse more than one line,
     * use $.csv2Array instead.
     */
    toArray: function (csv, options, callback) {
      // if callback was passed to options swap callback with options
      if (options !== undefined && typeof (options) === 'function') {
        if (callback !== undefined) {
          return console.error('You cannot 3 arguments with the 2nd argument being a function')
        }
        callback = options
        options = {}
      }

      options = (options !== undefined ? options : {})
      const config = {}
      config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false)
      config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator
      config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter
      const state = (options.state !== undefined ? options.state : {})

      // setup
      options = {
        delimiter: config.delimiter,
        separator: config.separator,
        onParseEntry: options.onParseEntry,
        onParseValue: options.onParseValue,
        state: state
      }

      const entry = $.csv.parsers.parseEntry(csv, options)

      // push the value to a callback if one is defined
      if (!config.callback) {
        return entry
      } else {
        config.callback('', entry)
      }
    },

    /**
     * $.csv.toArrays(csv)
     * Converts a CSV string to a javascript array.
     *
     * @param {String} csv The string containing the raw CSV data.
     * @param {Object} [options] An object containing user-defined options.
     * @param {Character} [separator] An override for the separator character. Defaults to a comma(,).
     * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote(").
     *
     * This method deals with multi-line CSV. The breakdown is simple. The first
     * dimension of the array represents the line (or entry/row) while the second
     * dimension contains the values (or values/columns).
     */
    toArrays: function (csv, options, callback) {
      // if callback was passed to options swap callback with options
      if (options !== undefined && typeof (options) === 'function') {
        if (callback !== undefined) {
          return console.error('You cannot 3 arguments with the 2nd argument being a function')
        }
        callback = options
        options = {}
      }

      options = (options !== undefined ? options : {})
      const config = {}
      config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false)
      config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator
      config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter

      // setup
      let data = []
      options = {
        delimiter: config.delimiter,
        separator: config.separator,
        onPreParse: options.onPreParse,
        onParseEntry: options.onParseEntry,
        onParseValue: options.onParseValue,
        onPostParse: options.onPostParse,
        start: options.start,
        end: options.end,
        state: {
          rowNum: 1,
          colNum: 1
        }
      }

      // onPreParse hook
      if (options.onPreParse !== undefined) {
        csv = options.onPreParse(csv, options.state)
      }

      // parse the data
      data = $.csv.parsers.parse(csv, options)

      // onPostParse hook
      if (options.onPostParse !== undefined) {
        data = options.onPostParse(data, options.state)
      }

      // push the value to a callback if one is defined
      if (!config.callback) {
        return data
      } else {
        config.callback('', data)
      }
    },

    /**
     * $.csv.toObjects(csv)
     * Converts a CSV string to a javascript object.
     * @param {String} csv The string containing the raw CSV data.
     * @param {Object} [options] An object containing user-defined options.
     * @param {Character} [separator] An override for the separator character. Defaults to a comma(,).
     * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote(").
     * @param {Boolean} [headers] Indicates whether the data contains a header line. Defaults to true.
     *
     * This method deals with multi-line CSV strings. Where the headers line is
     * used as the key for each value per entry.
     */
    toObjects: function (csv, options, callback) {
      // if callback was passed to options swap callback with options
      if (options !== undefined && typeof (options) === 'function') {
        if (callback !== undefined) {
          return console.error('You cannot 3 arguments with the 2nd argument being a function')
        }
        callback = options
        options = {}
      }

      options = (options !== undefined ? options : {})
      const config = {}
      config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false)
      config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator
      config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter
      config.headers = 'headers' in options ? options.headers : $.csv.defaults.headers
      options.start = 'start' in options ? options.start : 1

      // account for headers
      if (config.headers) {
        options.start++
      }
      if (options.end && config.headers) {
        options.end++
      }

      // setup
      let lines = []
      let data = []

      options = {
        delimiter: config.delimiter,
        separator: config.separator,
        onPreParse: options.onPreParse,
        onParseEntry: options.onParseEntry,
        onParseValue: options.onParseValue,
        onPostParse: options.onPostParse,
        start: options.start,
        end: options.end,
        state: {
          rowNum: 1,
          colNum: 1
        },
        match: false,
        transform: options.transform
      }

      // fetch the headers
      const headerOptions = {
        delimiter: config.delimiter,
        separator: config.separator,
        start: 1,
        end: 1,
        state: {
          rowNum: 1,
          colNum: 1
        },
        headers: true
      }

      // onPreParse hook
      if (options.onPreParse !== undefined) {
        csv = options.onPreParse(csv, options.state)
      }

      // parse the csv
      const headerLine = $.csv.parsers.splitLines(csv, headerOptions)
      const headers = $.csv.toArray(headerLine[0], headerOptions)

      // fetch the data
      lines = $.csv.parsers.splitLines(csv, options)

      // reset the state for re-use
      options.state.colNum = 1
      if (headers) {
        options.state.rowNum = 2
      } else {
        options.state.rowNum = 1
      }

      // convert data to objects
      for (let i = 0, len = lines.length; i < len; i++) {
        const entry = $.csv.toArray(lines[i], options)
        const object = {}
        for (let j = 0; j < headers.length; j++) {
          object[headers[j]] = entry[j]
        }
        if (options.transform !== undefined) {
          data.push(options.transform.call(undefined, object))
        } else {
          data.push(object)
        }

        // update row state
        options.state.rowNum++
      }

      // onPostParse hook
      if (options.onPostParse !== undefined) {
        data = options.onPostParse(data, options.state)
      }

      // push the value to a callback if one is defined
      if (!config.callback) {
        return data
      } else {
        config.callback('', data)
      }
    },

    /**
    * $.csv.fromArrays(arrays)
    * Converts a javascript array to a CSV String.
    *
    * @param {Array} arrays An array containing an array of CSV entries.
    * @param {Object} [options] An object containing user-defined options.
    * @param {Character} [separator] An override for the separator character. Defaults to a comma(,).
    * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote(").
    *
    * This method generates a CSV file from an array of arrays (representing entries).
    */
    fromArrays: function (arrays, options, callback) {
      // if callback was passed to options swap callback with options
      if (options !== undefined && typeof (options) === 'function') {
        if (callback !== undefined) {
          return console.error('You cannot 3 arguments with the 2nd argument being a function')
        }
        callback = options
        options = {}
      }

      options = (options !== undefined ? options : {})
      const config = {}
      config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false)
      config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator
      config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter

      let output = ''

      for (let i = 0; i < arrays.length; i++) {
        const line = arrays[i]
        const lineValues = []
        for (let j = 0; j < line.length; j++) {
          let strValue = (line[j] === undefined || line[j] === null) ? '' : line[j].toString()
          if (strValue.indexOf(config.delimiter) > -1) {
            strValue = strValue.replace(new RegExp(config.delimiter, 'g'), config.delimiter + config.delimiter)
          }

          let escMatcher = '\n|\r|S|D'
          escMatcher = escMatcher.replace('S', config.separator)
          escMatcher = escMatcher.replace('D', config.delimiter)

          if (strValue.search(escMatcher) > -1) {
            strValue = config.delimiter + strValue + config.delimiter
          }
          lineValues.push(strValue)
        }
        output += lineValues.join(config.separator) + '\n'
      }

      // push the value to a callback if one is defined
      if (!config.callback) {
        return output
      } else {
        config.callback('', output)
      }
    },

    /**
     * $.csv.fromObjects(objects)
     * Converts a javascript dictionary to a CSV string.
     *
     * @param {Object} objects An array of objects containing the data.
     * @param {Object} [options] An object containing user-defined options.
     * @param {Character} [separator] An override for the separator character. Defaults to a comma(,).
     * @param {Character} [delimiter] An override for the delimiter character. Defaults to a double-quote(").
     * @param {Character} [sortOrder] Sort order of columns (named after
     *   object properties). Use 'alpha' for alphabetic. Default is 'declare',
     *   which means, that properties will _probably_ appear in order they were
     *   declared for the object. But without any guarantee.
     * @param {Character or Array} [manualOrder] Manually order columns. May be
     * a strin in a same csv format as an output or an array of header names
     * (array items won't be parsed). All the properties, not present in
     * `manualOrder` will be appended to the end in accordance with `sortOrder`
     * option. So the `manualOrder` always takes preference, if present.
     *
     * This method generates a CSV file from an array of objects (name:value pairs).
     * It starts by detecting the headers and adding them as the first line of
     * the CSV file, followed by a structured dump of the data.
     */
    fromObjects: function (objects, options, callback) {
      // if callback was passed to options swap callback with options
      if (options !== undefined && typeof (options) === 'function') {
        if (callback !== undefined) {
          return console.error('You cannot 3 arguments with the 2nd argument being a function')
        }
        callback = options
        options = {}
      }

      options = (options !== undefined ? options : {})
      const config = {}
      config.callback = ((callback !== undefined && typeof (callback) === 'function') ? callback : false)
      config.separator = 'separator' in options ? options.separator : $.csv.defaults.separator
      config.delimiter = 'delimiter' in options ? options.delimiter : $.csv.defaults.delimiter
      config.headers = 'headers' in options ? options.headers : $.csv.defaults.headers
      config.sortOrder = 'sortOrder' in options ? options.sortOrder : 'declare'
      config.manualOrder = 'manualOrder' in options ? options.manualOrder : []
      config.transform = options.transform

      if (typeof config.manualOrder === 'string') {
        config.manualOrder = $.csv.toArray(config.manualOrder, config)
      }

      if (config.transform !== undefined) {
        const origObjects = objects
        objects = []

        for (let i = 0; i < origObjects.length; i++) {
          objects.push(config.transform.call(undefined, origObjects[i]))
        }
      }

      let props = $.csv.helpers.collectPropertyNames(objects)

      if (config.sortOrder === 'alpha') {
        props.sort()
      }

      if (config.manualOrder.length > 0) {
        const propsManual = [].concat(config.manualOrder)

        for (let p = 0; p < props.length; p++) {
          if (propsManual.indexOf(props[p]) < 0) {
            propsManual.push(props[p])
          }
        }
        props = propsManual
      }

      let line
      const output = []
      let propName
      if (config.headers) {
        output.push(props)
      }

      for (let o = 0; o < objects.length; o++) {
        line = []
        for (let p = 0; p < props.length; p++) {
          propName = props[p]
          if (propName in objects[o] && typeof objects[o][propName] !== 'function') {
            line.push(objects[o][propName])
          } else {
            line.push('')
          }
        }
        output.push(line)
      }

      // push the value to a callback if one is defined
      return $.csv.fromArrays(output, options, config.callback)
    }
  }

  // Maintenance code to maintain backward-compatibility
  // Will be removed in release 1.0
  $.csvEntry2Array = $.csv.toArray
  $.csv2Array = $.csv.toArrays
  $.csv2Dictionary = $.csv.toObjects

  // CommonJS module is defined
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = $.csv
  }
}).call(this)
